﻿namespace JsViewEngine
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.IO;
    using System.Threading;

    using Jurassic;
    using Jurassic.Library;

    internal class JurassicEngineInstance
    {
        private const string RenderFunction =
            @"var render = function(templateHash, data, context) { 
    return jsviews.templates[templateHash].render(data, context); 
};";

        private const string CompileRenderFunction =
            @"var compileRender = function(templateHash, template, data, context) { 
    return (jsviews.templates[templateHash] || jsviews.templates(templateHash, template)).render(data, context); 
};";

        private const string SetViewConvertersFunctionFormat = 
@"jsviews.converters({0}());";

        private readonly HashSet<string> compiledTemplates = new HashSet<string>();

        private readonly string jsrenderPath;
        private readonly string viewconvertersPath;

        private DateTime? jsrenderWriteTime;
        private DateTime? viewconvertersWriteTime;

        private ScriptEngine cachedScriptEngine;


        public JurassicEngineInstance(JurassicEngineInstanceOptions jurassicEngineInstanceOptions)
        {
            this.jsrenderPath = jurassicEngineInstanceOptions.JsrenderPath;
            this.viewconvertersPath = jurassicEngineInstanceOptions.ViewConvertersPath;
        }

        private ScriptEngine GetEngineInstance()
        {
            bool refresh = false;

            var stopwatch = Stopwatch.StartNew();
            ScriptEngine scriptEngine = this.cachedScriptEngine;
            if (scriptEngine == null)
            {
                scriptEngine = new ScriptEngine();
                this.cachedScriptEngine = scriptEngine;
                scriptEngine.Execute(RenderFunction);
                scriptEngine.Execute(CompileRenderFunction);

                stopwatch.Stop();
                Debug.WriteLine(
                    "Jurassic Engine: created on thread: {0} in {1:N4} ms.",
                    Thread.CurrentThread.ManagedThreadId,
                    stopwatch.ElapsedMilliseconds);

                refresh = true;
            }

            if (string.IsNullOrWhiteSpace(this.viewconvertersPath) == false)
            {
                var writeTime = File.GetLastWriteTimeUtc(this.viewconvertersPath);
                if (this.viewconvertersWriteTime != writeTime)
                {
                    refresh = true;
                    this.viewconvertersWriteTime = writeTime;
                }
            }

            var currentJsrenderWriteTime = File.GetLastWriteTimeUtc(this.jsrenderPath);

            if (currentJsrenderWriteTime != this.jsrenderWriteTime)
            {
                refresh = true;
                this.jsrenderWriteTime = currentJsrenderWriteTime;
            }

            // if the files haven't changed, then lets just return the cached instance
            if (refresh == false)
            {
                return scriptEngine;
            }

            // else we need to refresh the jsrender.js definition
            stopwatch.Restart();
            scriptEngine.ExecuteFile(this.jsrenderPath);
            scriptEngine.ExecuteFile(this.viewconvertersPath);
            scriptEngine.Execute(string.Format(SetViewConvertersFunctionFormat, Path.GetFileNameWithoutExtension(this.viewconvertersPath)));

            stopwatch.Stop();
            Debug.WriteLine(
                "Jurassic Engine: refreshed jsrender.js on thread: {0} in {1:N4} ms.",
                Thread.CurrentThread.ManagedThreadId,
                stopwatch.ElapsedMilliseconds);

            return scriptEngine;
        }

        public string Render(string templateHash, string template, object data, string context)
        {
            var stopwatch = Stopwatch.StartNew();

            var dataString = Newtonsoft.Json.JsonConvert.SerializeObject(data);
            ScriptEngine scriptEngine = this.GetEngineInstance();

            var dataObject = (ObjectInstance)scriptEngine.Evaluate(dataString);

            var contextObject = context != null ? (ObjectInstance)scriptEngine.Evaluate(context) : null;

            string result;
            string timerMessageFormatted;
            if (this.compiledTemplates.Contains(templateHash))
            {
                result = Render(scriptEngine, templateHash, dataObject, contextObject);
                timerMessageFormatted = "Jurassic Engine: executed jsRender (pre-compiled) in {0:N4} ms";
            }
            else
            {
                result = CompileRender(scriptEngine, templateHash, template, dataObject, contextObject);
                this.compiledTemplates.Add(templateHash);

                Debug.WriteLine("Jurassic Engine: Compiled template with hash of: {0}", (object)templateHash);
                timerMessageFormatted = "Jurassic Engine: executed jsRender in {0:N4} ms";
            }

            stopwatch.Stop();
            Debug.WriteLine(timerMessageFormatted, stopwatch.ElapsedMilliseconds);
            return result;
        }

        private static string Render(ScriptEngine scriptEngine, string templateHash, ObjectInstance dataObject, ObjectInstance contextObject)
        {
            return scriptEngine.CallGlobalFunction<string>("render", templateHash, dataObject, contextObject);
        }

        private static string CompileRender(ScriptEngine scriptEngine, string templateHash, string template, ObjectInstance dataObject, ObjectInstance contextObject)
        {
            return scriptEngine.CallGlobalFunction<string>("compileRender", templateHash, template, dataObject, contextObject);
        }
    }
}